package com.phoenix;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class CloudOauth2ServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(CloudOauth2ServerApplication.class, args);
    }

}

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class ServerSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("john").password("123").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().anonymous().disable()
                .addFilter(usernamePasswordAuthenticationFilter())
                .authorizeRequests()
                .anyRequest().fullyAuthenticated()
                .and().formLogin().loginPage("/login").loginProcessingUrl("/oauth/checkLogin").permitAll();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public ServerUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {

        ServerUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter
                = new ServerUsernamePasswordAuthenticationFilter();
        usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
        return usernamePasswordAuthenticationFilter;
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**", "/**/favicon.ico", "/captcha.jpg", "/login");
    }
}

@Configuration
@EnableAuthorizationServer
class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private OAuthAuthorizationCodeServices oAuthAuthorizationCodeServices;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setClientDetailsService(customClientDetailsService());
        tokenServices.setAccessTokenValiditySeconds(2592000);
        tokenServices.setRefreshTokenValiditySeconds(328500000);
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        endpoints.tokenServices(tokenServices).authorizationCodeServices(oAuthAuthorizationCodeServices)
                .authenticationManager(authenticationManager);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(customClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("isAnonymous() || hasRole('ROLE_TRUSTED_CLIENT')") // permitAll()
                .checkTokenAccess("hasRole('TRUSTED_CLIENT')"); // isAuthenticated()
    }

    @Bean
    public TokenStore tokenStore() {

        return new RedisTokenStore(redisTemplate.getConnectionFactory());
    }

    @Bean
    public CustomClientDetailsService customClientDetailsService() {
        return new CustomClientDetailsService(dataSource, redisTemplate);
    }

    protected class CustomClientDetailsService extends JdbcClientDetailsService {

        private RedisTemplate redisTemplate;

        public CustomClientDetailsService(DataSource dataSource, RedisTemplate redisTemplate) {
            super(dataSource);
            this.redisTemplate = redisTemplate;
        }

        @Override
        public ClientDetails loadClientByClientId(String clientId) {

            ClientDetails clientDetails = (ClientDetails) redisTemplate.opsForValue().get(clientId);
            if (clientDetails == null) {
                clientDetails = super.loadClientByClientId(clientId);
                redisTemplate.opsForValue().set(clientId, clientDetails, 30 * 60, TimeUnit.MINUTES);
            }
            return clientDetails;
        }
    }
}

@Component
class OAuthAuthorizationCodeServices implements AuthorizationCodeServices {

    private static final String PRE_KEY = "CODE_";

    @Autowired
    private RedisTemplate redisTemplate;

    private RandomValueStringGenerator generator = new RandomValueStringGenerator(16);

    public String createAuthorizationCode(OAuth2Authentication authentication) {
        String code = generator.generate();
        redisTemplate.opsForValue().set(PRE_KEY + code, authentication, 10 * 60, TimeUnit.MINUTES);
        return code;
    }

    public OAuth2Authentication consumeAuthorizationCode(String code)
            throws InvalidGrantException {
        OAuth2Authentication auth = (OAuth2Authentication) redisTemplate.opsForValue().get(PRE_KEY + code);
        if (auth == null) {
            throw new InvalidGrantException("Invalid authorization code: " + code);
        }
        return auth;
    }
}